feat/email-registration - #212
Conversation
81894da to
66e733f
Compare
brh28
left a comment
There was a problem hiding this comment.
There's already the following definition in the graphql schema, which appears to the same or similar to what's being done in this PR:
userEmailRegistrationInitiate(input: UserEmailRegistrationInitiateInput!): UserEmailRegistrationInitiatePayload!
userEmailRegistrationValidate(input: UserEmailRegistrationValidateInput!): UserEmailRegistrationValidatePayload!
These definitions do not allow for a email registration without an existing account. The choice was to modify these, to handle both cases (with account and without account) or create a new schema definition for new accounts. |
66e733f to
90f293e
Compare
|
@brh28 please review |
| // so that if one fails, the other is rolled back | ||
|
|
||
| // 1. Update user record with email (deviceId is preserved via spread) | ||
| const userUpdated = await UsersRepository().findById(userId) |
There was a problem hiding this comment.
Lines 60-65 can be consolidated to a single database update. The function would be something like:
addEmail: (userId, email) => db.updateOne( { userId }, { $set: { email })
| userId: UserId | ||
| email: EmailAddress | ||
| }): Promise<Account | RepositoryError> => { | ||
| // TODO: ideally both 1. and 2. should be done in a transaction, |
There was a problem hiding this comment.
I don't believe there's a way to make this atomic unless we completely rewrite our data model
|
|
||
| import { createAccountWithEmailIdentifier } from "@app/accounts" | ||
|
|
||
| export const createAccountFromEmailRegistrationPayload = async ({ |
There was a problem hiding this comment.
where is this function being called?
There was a problem hiding this comment.
nowhere! apparently its leftover from a previous attempt at getting this to work. Removed the file and the export reference
brh28
left a comment
There was a problem hiding this comment.
Can we do a code walk through on this one? I'm having a hard time following
|
Related to: #237 |
|
@brh28 I think this is ready for a code review again, since we patched the orphaned accounts issue. |
| const accountsRepo = AccountsRepository() | ||
| let account = await accountsRepo.findByUserId(kratosUserId) | ||
|
|
||
| if (account instanceof Error) { |
There was a problem hiding this comment.
probably should be checking for a specific response, such as AccountNotFoundError
| } else { | ||
| // Create new identity with email | ||
| const createIdentityBody = { | ||
| credentials: { password: { config: { password } } }, |
| } | ||
|
|
||
| // Send OTP code via recovery flow | ||
| const { data: recoveryFlow } = await kratosPublic.createNativeRecoveryFlow() |
There was a problem hiding this comment.
verify we want createNativeRecoveryFlow rather than createNativeRegistrationFlow
| type: [String], | ||
| }, | ||
| deviceId: { | ||
| email: { |
There was a problem hiding this comment.
emails are already stored in Kratos. Is there any reason not to use the postrgres database here?
- New GraphQL mutations: newUserEmailRegistrationInitiate and newUserEmailRegistrationValidate for email-only account creation - Kratos integration: Successfully using email recovery flow for OTP delivery - Account creation: Fixed critical bug where accounts weren't being created after validation - Code cleanup: Removed all debug console.log statements ✅ Key Changes Made 1. Fixed validation logic - Changed from checking User existence to Account existence 2. Proper account creation - Creates account with wallets when none exists 3. Clean production code - Removed debug statements for production readiness
This commit consolidates all TypeScript fixes required after rebasing the email-registration feature branch on main: - Update core Account and Wallet mocks with mandatory properties (npub, lnurlp) - Adapt to upstream API changes (@ory/client IdentityState → IdentityStateEnum) - Fix test infrastructure mock type conversions and exports - Replace deleted BTC wallet functions with USD equivalents - Use factory functions for branded types (OnChainAddress, FractionalCentAmount) - Fix OffersManager constructability and CSV export method names - Add type casts for transaction and wallet type incompatibilities Result: yarn tsc --noEmit returns 0 errors
fdea49c to
204ac45
Compare
…lidation deferral - Add ValidationError import to redeem-invite.ts to fix TypeScript errors - Document that email validation is deferred until email-only registration feature is available (see PR #212)
|
Closed as deferred |
…lidation deferral - Add ValidationError import to redeem-invite.ts to fix TypeScript errors - Document that email validation is deferred until email-only registration feature is available (see PR #212)
…462) * feat: Implement referral system with Email/SMS/WhatsApp invites Add comprehensive invite-friend feature allowing users to invite friends via Email, SMS, or WhatsApp. **User-Facing Features:** - Create invites: Users can send invites via Email (SendGrid), SMS, or WhatsApp (Twilio) - Redeem invites: New users can redeem invites within 1 hour of account creation - Preview invites: Unauthenticated endpoint to preview invite before registration - Rate limiting: 10 invites/day per user, 3 invites/day per target contact (Redis-based) - 24-hour invite expiration with Firebase Dynamic Links support **Admin Features:** - View invite details with inviter/redeemer information - List and filter invites by status and inviter - Paginated invite queries **Technical Implementation:** - MongoDB schema for invite tracking with secure token hashing (SHA-256) - Notification service supporting Email, SMS, and WhatsApp - Contact validation for email/phone formats - Deep linking support via Firebase Dynamic Links - Comprehensive test coverage (unit & integration tests) **Security:** - Tokens are 40-character random strings with only SHA-256 hash stored - Contact verification ensures invite sent to correct recipient - Account age validation (< 1 hour) for new user redemption - Self-redemption prevention * fix: improve invite feature consistency and remove code duplication - Fix rate limit key inconsistency between admin functions and rate limiter service (use RateLimitPrefix constants) - Refactor GraphQL createInvite mutation to use @app/invite layer instead of duplicating business logic - Add index on redeemedById field in invite schema for query performance - Make new-user invite redemption window configurable via NEW_USER_INVITE_WINDOW_HOURS constant (default 24 hours, was 1 hour) - Standardize token generation to use 20-byte (40-char) tokens * fix: improve type safety in invite feature - Add INVITE_TOKEN_LENGTH constant (40 chars) to domain - Add InviteToken branded type with checkedToInviteToken validator - Fix token length check in app/invite/redeem-invite.ts (was 64, should be 40) - Replace magic number checks with typed validation in GraphQL mutations - Use checkedToAccountId instead of unsafe `as AccountId` cast in invite-preview * fix(invite): add missing ValidationError import and document email validation deferral - Add ValidationError import to redeem-invite.ts to fix TypeScript errors - Document that email validation is deferred until email-only registration feature is available (see PR #212) * chore(invite): regenerate admin GraphQL SDL after rebase Rebasing feat/invite onto main took main's admin schema.graphql (--ours) during conflict resolution; write-sdl regenerates it to include the invite admin types (AdminInvite, invitesList, inviteById). Public SDL already carried the invite types via clean auto-merge. Full `yarn build` compiled cleanly, verifying the rebase conflict resolutions typecheck. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg * test(invite): add backend unit tests for invite/refer feature 80 tests / 8 suites, fully mocked (no infra): domain validation + invite constants/token checks, hash/token generation, app-layer create-invite, redeem-invite, rate-limits, queries, and admin ops. Covers success + error paths (invalid contact, rate-limited, duplicate, expired, self-redeem, etc.). GraphQL resolver wiring + Redis-backed rate-limiter left for test/flash/integration (need infra). Note: redeem-invite's reward-crediting is still a TODO (redemption only flips status to ACCEPTED). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg * feat(invite): tiered referral reward payout on Bridge KYC approval Implements the referral reward the invite feature only stubbed. When an invited user's Bridge KYC is approved (they gain a US account), both the inviter and the invitee are paid a tiered USD reward, funded from a dedicated 'rewards' wallet. - New 'rewards' account role (AccountRoles, mongoose enum, AdminRole); assign it to the funding account via a direct mongo write. Resolved with AccountsRepository().findByRole. - Tiered amount by global referral sequence (atomic counter): 1-100 -> $5, 101-600 -> $2.50, 601+ -> $1. Ops-tunable via the new referralReward config block (default DISABLED, so nothing pays until a rewards wallet is assigned). - Trigger: the once-only pending->approved transition in the Bridge KYC webhook (CAS-guarded). Payout via intraledgerPaymentSendWalletIdForUsdWallet. - Idempotent + fail-closed: atomic claim on the invite, per-party inviter/inviteeRewardedAt, never double-pays; a failed/partial payout is recorded (rewardStatus) for manual reconciliation and never throws into or blocks KYC approval. Admin visibility via new AdminInvite reward fields. Tests: 23 unit (9 tier boundaries + 14 payout paths incl. idempotency, partial, failed, disabled). tsc-check clean; admin SDL regenerated. (Also fixes a latent tsc-check type error in the admin invite spec's mock.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg * fix(invite): make the feature CI-green (scope-map, lint, module-load) The invite feature was never CI-green (never merged), so PR #462 surfaced several gaps plus two regressions from the reward payout: - api-key scope-map: register createInvite (authed mutation) as BLOCKED so the deny-by-default completeness test passes. redeemInvite/invitePreview are in the unauthed schema block, so they are (correctly) not authed root fields. - award-referral-reward: lazy-import send-intraledger inside payParty so merely importing @app/invite no longer pulls the IBEX client (baseLogger.child at init) — was breaking kyc.spec + create-invite.spec at module load. - ops-events-hooks.spec @config mock: add getInviteCreateAttemptLimits/ getInviteTargetAttemptLimits (domain/rate-limit evaluates them at load). - prettier/eslint: format the never-linted invite files; drop unused imports (InviteToken; redeem-invite mutation dead imports); type two anys in services/notification. Gates: eslint 0 errors, tsc-check + tsc-check-noimplicitany clean, full unit suite 1304 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg * fix(invite): harden reward payout per review findings - Stuck-claim recovery: the atomic claim stamps rewardClaimedAt, and any unexpected throw after the claim downgrades it to 'failed' with a rewardError instead of stranding an invisible 'processing' row. - IBEX Pending is a distinct non-terminal 'pending' rewardStatus (new enum value). Per-party timestamps are still set for pending parties — fail-closed, a re-run can never double-pay — but ops now sees it needs re-checking instead of it being counted as terminally paid. - Payouts fund from the rewards account's USDT wallet first (the active cash wallet), falling back to USD, and recipients are resolved strictly in the funding wallet's currency (send-intraledger rejects cross-currency sends). - Tier fail-safe: a schedule missing its unbounded sentinel pays 0 past the last bound instead of silently over-paying forever. Tests: award spec 14->18 (post-claim throw, pending semantics, wallet preference/currency-match, claim stamp), tier fail-safe boundaries. Gates: scoped jest 130/130, tsc-check + noimplicitany + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg * fix(invite): close re-review findings — one-reward-per-invitee invariant + 8 more F1 (blocking): one reward per invitee, enforced in three layers — redemption rejects a second redemption per account, a unique partial index on redeemedById closes the race (duplicate-key treated as already-redeemed), and the award path skips accounts that already have any rewardStatus so a Bridge KYC approved->under_review->approved flap can never pay a second accumulated invite. F2: the post-claim catch preserves payment evidence — a throw mid-payout now records partial with the paid party's timestamp instead of failed-with-nothing (manual reconciliation can no longer double-pay a paid party). F3: redeemInvite moved to the authed block + scope-map BLOCKED (was reachable by read-scoped API keys via the unauthed shield gap). SDL unchanged. F4: raw invite tokens and Twilio auth-token fragments no longer logged. F5: revoked/EXPIRED-status invites rejected at redeem + preview, independent of the date check. F6: Timestamp scalar accepts ISO strings again (parseInt regression silently turned admin cutover scheduledAt into 1970); pure digits = epoch seconds, invalid input errors. Pinned by a new scalar spec. F7: admin invitesList — ObjectId cast for the pipeline filter (was always empty) and validated _id-cursor pagination (was parseInt(after,16) nonsense). F8: a failed invite notification deletes the invite and returns an error instead of burning the contact's 24h dup-window with nothing sent. F9: dead app-layer redeem module deleted; the LIVE resolver now has a 14-case spec (token/window/phone/EMAIL/self/race/revoked/success paths). Full unit suite 157 suites / 1326 passed / 0 failed; tsc-check, noimplicitany, eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg * fix: 'unparseable' -> 'unparsable' (typos CI) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg * fix(invite): check ACCEPTED before the date-expiry flip on redeem A post-expiry replay of an already-redeemed invite's token used to overwrite ACCEPTED with EXPIRED — stranding the pending reward and, now that accounts are limited to one redemption ever, permanently costing the account its referral. Reorder the checks; regression test pins ACCEPTED + no save. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7z7o9J18BWbUnYJcemMtg --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Dread <bobodread@bobodread.com>
Summary
Implements email-only authentication flow for new user registration, allowing users to sign up with just an email address (no phone required).
Key Changes:
newUserEmailRegistrationInitiateandnewUserEmailRegistrationValidateemail_no_password_v0)How It Works
newUserEmailRegistrationInitiatewith emailnewUserEmailRegistrationValidatewith flowId + codeFiles Changed (43 files, +855/-216)
Core Implementation:
src/graphql/public/root/mutation/new-user-email-registration-*.ts- GraphQL mutationssrc/app/authentication/email.ts- Email authentication logicsrc/services/kratos/auth-email-no-password.ts- Kratos integrationsrc/app/accounts/create-account.ts- Account creation updatessrc/app/accounts/upgrade-device-account.ts- Device → Email upgradesrc/domain/authentication/registration-payload-validator.ts- Validation logicdev/ory/kratos.yml- Kratos configurationTest Infrastructure:
Known Issues / Follow-up Tickets
🔴 HIGH PRIORITY (security review needed)
Account Enumeration Vulnerability
src/services/kratos/auth-email-no-password.tslines 73-78createIdentityForEmailRegistration()reveals whether email is already registeredMissing TOTP Flow Completion
newUserEmailRegistrationValidatereturnstotpRequiredbut no follow-up verificationRace Condition in Account Creation
new-user-email-registration-validate.tslines 63-78🟡 MEDIUM PRIORITY (tech debt)
SchemaIdType.EmailNoPasswordV0enumupgrade-device-account.ts🟢 LOW PRIORITY
Testing
yarn tsc --noEmit- 0 errors)Checklist